iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
自我挑戰組

AI Agent 從零開始系列 第 9 篇

讓 Agent 學會使用多個 Tools

  • 分享至 

  • xImage
  •  

讓 Agent 能協調、組合並穩定使用多個工具(Multi-Tool Coordination),是將簡單的問答機器人轉變為複雜自動化系統的關鍵。

當工具數量從 1 個增加到 5 個、甚至幾十個時,Agent 會面臨三大挑戰:

  1. 工具選擇混淆(Tool Selection Overhead):選錯工具或傳入錯誤參數。
  2. 多步驟依賴(Sequential Dependency):後一個工具的輸入,依賴前一個工具的輸出。
  3. 平行執行需求(Parallel Execution):同時需要查詢多個無關資料時,單步執行太慢。

1. 多工具 Agent 的架構設計

要實現穩定的多工具 Agent,系統架構需要具備:

  • 多工具並行 (Parallel Tool Calls):單一推理輪次中,模型能同時發出多個 API 請求。
  • 狀態歷史與依賴鏈 (State & Chains):將每次工具執行的結果 (role: "tool") 妥善保存於對話歷史中,讓模型能進行 cross-tool 的上下文推理。
                       ┌─── Tool 1 (搜尋 API) ───┐
[LLM] ──發出多個 ToolCall ──┼─── Tool 2 (天氣 API) ───┼──> [App 執行並彙整] ──> [LLM 總結]
                       └─── Tool 3 (計算 API) ───┘


2. 實作:多工具 Agent (支援 Parallel Execution & Error Recovery)

我們基於前面建構的 ToolRegistry,撰寫一個能夠靈活處理多工具相依性與並行呼叫的 Agent Engine。

import json
from openai import OpenAI
from typing import List, Dict, Any

# 假設已載入前面設計的 ToolRegistry 類別
# registry = ToolRegistry()

class MultiToolAgent:
    def __init__(self, model_name: str, registry):
        self.client = OpenAI()
        self.model_name = model_name
        self.registry = registry

    def run(self, user_goal: str, max_steps: int = 10) -> str:
        """
        執行 Agent Loop,允許多步驟與多工具組合執行
        """
        messages: List[Dict[str, Any]] = [
            {
                "role": "system",
                "content": (
                    "你是一個高效率的 AI 助理。你可以組合使用多個工具來達成使用者的目標。"
                    "如果多個工具之間沒有依賴關係,你可以一次發出多個工具呼叫以提高效率。"
                )
            },
            {"role": "user", "content": user_goal}
        ]

        print(f"🎯 任務目標: {user_goal}\n" + "="*50)

        for step in range(1, max_steps + 1):
            print(f"\n🔄 [Step {step}] Agent 思考中...")

            # 1. 呼叫 LLM 進行思考 (Reasoning)
            response = self.client.chat.completions.create(
                model=self.model_name,
                messages=messages,
                tools=self.registry.get_openai_tools(),
                tool_choice="auto"
            )

            response_message = response.choices[0].message
            messages.append(response_message) # 記錄 LLM 的思考/發出的 Call

            tool_calls = response_message.tool_calls

            # 2. 終止條件:LLM 評估不需要再呼叫工具,輸出最終答案
            if not tool_calls:
                print(f"\n✅ 任務完成!最終回答:\n{response_message.content}")
                return response_message.content

            # 3. 執行工具 (Acting Phase) - 支援單輪多工具併發
            print(f"🛠️ 本輪發出 {len(tool_calls)} 個工具呼叫:")

            for tool_call in tool_calls:
                tool_name = tool_call.function.name
                args_str = tool_call.function.arguments
                tool_id = tool_call.id

                print(f"   ├─ 呼叫: {tool_name} | 參數: {args_str}")

                # 透過 Registry 派發執行
                observation = self.registry.dispatch(tool_name, args_str)

                print(f"   └─ 觀察 (Observation): {observation[:80]}..." if len(observation) > 80 else f"   └─ 觀察 (Observation): {observation}")

                # 將每個工具執行結果放入 Context,並用 tool_call_id 精確對應
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_id,
                    "name": tool_name,
                    "content": observation
                })

        return "⚠️ 超過最大步驟上限,任務中斷。"


3. 多工具實戰場景展示

我們為 Registry 註冊三個功能各異的工具,模擬「資料檢索 $\rightarrow$ 數據計算 $\rightarrow$ 發送報告」的工作流:

registry = ToolRegistry()

@registry.register(description="根據使用者名稱查詢其在系統中的消費記錄金額列表")
def get_user_expenses(username: str) -> list:
    mock_db = {
        "alice": [120, 450, 3200, 880],
        "bob": [50, 60]
    }
    return mock_db.get(username.lower(), [])

@registry.register(description="計算一組數字列表的平均值與總和")
def calculate_stats(numbers: list[float]) -> dict:
    if not numbers:
        return {"sum": 0, "avg": 0}
    total = sum(numbers)
    return {"sum": total, "avg": round(total / len(numbers), 2)}

@registry.register(description="發送通知報告給指定使用者")
def send_notification(username: str, message: str) -> str:
    # 模擬通知 API
    return f"通知已成功發送給 {username}:[{message}]"

測試複雜多工具指令:

agent = MultiToolAgent(model_name="gpt-4o-mini", registry=registry)

prompt = "請幫我查 Alice 的消費記錄,計算總花費與平均花費,最後發送通知告知 Alice 她這期的消費統計。"
agent.run(prompt)


4. 執行軌跡(Agent 多工具協調過程)

  1. 步驟 1:依賴識別與單一查詢: LLM 發現資料不足,先取消費紀錄.
    LLM 發出 1 個 Tool Call:get_user_expenses(username="Alice")。

Observation: [120, 450, 3200, 880]

  1. 步驟 2:管道對接與計算: 將步驟 1 的輸出傳入步驟 2.
    LLM 拿到數字陣列後,發現需要計算,發出 Tool Call:calculate_stats(numbers=[120, 450, 3200, 880])。

Observation: {"sum": 4650, "avg": 1162.5}

  1. 步驟 3:跨工具發送動作: 整合計算結果並觸發下游系統.
    LLM 組合計算好的數字,發出 Tool Call:send_notification(username="Alice", message="您本期總花費為 4650 元,平均每筆消費 1162.5 元。")。

Observation: "通知已成功發送..."

  1. 步驟 4:任務完成總結: 終止 Tool Loop 並回報使用者.
    LLM 確認所有子任務完成,停止 Tool Call,回應:「已成功為您查詢 Alice 的消費紀錄並發送通知...」。

5. 多工具系統的擴充瓶頸與最佳實踐(Best Practices)

當工具數量持續增加時,開發者會遇到以下問題與解決策略:

1. 工具數量膨脹 (Context Window Pollution)

  • 問題:如果在 Prompt 裡放了 50 個工具的 JSON Schema,會大量浪費 Context Window 並干擾模型的推理精準度。
  • 解法:Tool RAG / Dynamic Tool Retrieval
  • 不要一次給模型所有工具。
  • 將工具的 description 做 Vector Embedding 存入向量庫。
  • 根據使用者的問題,動態檢索出 Top 3-5 個最相關的工具 塞給 LLM。

2. 工具命名與描述的工程化 (Description Engineering)

  • 規則:
  • 名稱語意化:get_user_expenses 比 fetch_data 好。
  • 明確說明「何時用」與「何時不用」:在 description 裡寫明邊界,例如:「當需要計算統計數據時使用,不要用來發送郵件」。
  • 參數範例 (Examples):在參數的 description 提供範例格式(如 "YYYY-MM-DD")。

3. 人類介入防護 (Human-in-the-Loop, HITL)

  • 針對唯讀工具(如 get_expenses、search),Agent 可以全自主執行。
  • 針對寫入/副作用工具(如 send_email、transfer_money),在 Agent 決定 Call Tool 時,系統發出 Pause 訊號,跳出 UI 供使用者確認(Approve / Reject)後再繼續運作。

上一篇
自己設計第一個 Tool System
下一篇
Tool 不只是 Function
系列文
AI Agent 從零開始 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言